using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;


namespace Deferred_Rendering
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class Deferred_Renderer_One : Renderer
    {
        private Vector2 halfPixel;

        private QuadRenderComponent quadRenderer;

        private Model sphereModel;

        //stores colour and the specular intensity
        private RenderTarget2D colourRT;

        //stores normals and specular power
        private RenderTarget2D normalRT;

        //stores the lightmap
        private RenderTarget2D lightRT;

        //stores depth/position
        private RenderTarget2D depthRT;

        private Effect clearBufferEffect;
        private Effect directionalLightEffect;
        private Effect pointLightEffect;
        private Effect finalCombineEffect;

        public Deferred_Renderer_One(Game game) : base(game)
        {
            // TODO: Construct any child components here
        }

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {          
            quadRenderer = new QuadRenderComponent(Game);
            
            Game.Components.Add(quadRenderer); 

            base.Initialize();
        }

        protected override void LoadContent()
        {
            int backBufferWidth = GraphicsDevice.PresentationParameters.BackBufferWidth;
            int backBufferHeight = GraphicsDevice.PresentationParameters.BackBufferHeight;

            halfPixel.X = 0.5f / (float)backBufferWidth;
            halfPixel.Y = 0.5f / (float)backBufferHeight;

            //setting the size of the rendertargets and setting the format
            //for both colour and normals we need at least 3 distinct components and so as not to bloat the g-buffer we'll go with 32 bit formats
            //both the colour and normal rendertarget use the 32 bit SurfaceFormat.Color format, so we can store 8 bits of precision
            //for the r, g and b components and x, y and z components respectively
            //the remaining 8 bits of each are used for the specular intensity and specular power
            colourRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, 1, SurfaceFormat.Color);
            normalRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, 1, SurfaceFormat.Color);

            //we'll use a single 32 bit component for the depth/position data
            depthRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, 1, SurfaceFormat.Single);

            //also 32 bit format for lightmap
            lightRT = new RenderTarget2D(GraphicsDevice, backBufferWidth, backBufferHeight, 1, SurfaceFormat.Color);

            clearBufferEffect = Game.Content.Load<Effect>("Deferred Renderer Shader Files\\ClearGBuffer");
            directionalLightEffect = Game.Content.Load<Effect>("Deferred Renderer Shader Files\\DirectionalLight");
            pointLightEffect = Game.Content.Load<Effect>("Deferred Renderer Shader Files\\PointLight");
            finalCombineEffect = Game.Content.Load<Effect>("Deferred Renderer Shader Files\\CombineFinal");

            sphereModel = Game.Content.Load<Model>("Models\\sphere");

            base.LoadContent();
        }

        //sets up our three rendertargets with the graphics device
        private void SetGBuffer()
        {
            GraphicsDevice.SetRenderTarget(0, colourRT);
            GraphicsDevice.SetRenderTarget(1, normalRT);
            GraphicsDevice.SetRenderTarget(2, depthRT);
        }

        //switching rendertargets resolves previous targets, so we do this for all rendertargets
        private void ResolveGBuffer()
        {
            GraphicsDevice.SetRenderTarget(0, null);
            GraphicsDevice.SetRenderTarget(1, null);
            GraphicsDevice.SetRenderTarget(2, null);
        }

        //sets colour RT to black, normat RT to grey ( since values range from 0.0 to 1.0 and we need them to be from -1.0 to 1.0, grey (0.5) equates to 0.0 ) and depth to white
        private void ClearGBuffer()
        {
            clearBufferEffect.Begin();

            clearBufferEffect.Techniques[0].Passes[0].Begin();

            quadRenderer.Render(Vector2.One * -1, Vector2.One);

            clearBufferEffect.Techniques[0].Passes[0].End();

            clearBufferEffect.End();
        }

        private void DrawDirectionalLight(Vector3 lightDirection, Color color)
        {
            //set all parameters
            directionalLightEffect.Parameters["colorMap"].SetValue(colourRT.GetTexture());
            directionalLightEffect.Parameters["normalMap"].SetValue(normalRT.GetTexture());
            directionalLightEffect.Parameters["depthMap"].SetValue(depthRT.GetTexture());
            directionalLightEffect.Parameters["lightDirection"].SetValue(lightDirection);
            directionalLightEffect.Parameters["Color"].SetValue(color.ToVector3());
            directionalLightEffect.Parameters["cameraPosition"].SetValue(camera.Position);
            directionalLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(camera.View * camera.Projection));
            directionalLightEffect.Parameters["halfPixel"].SetValue(halfPixel);
            
            directionalLightEffect.Begin();
            directionalLightEffect.Techniques[0].Passes[0].Begin();

            //draw a full-screen quad
            quadRenderer.Render(Vector2.One * -1, Vector2.One);

            directionalLightEffect.Techniques[0].Passes[0].End();
            directionalLightEffect.End();
        }

        private void DrawPointLight(Vector3 lightPosition, Color color, float lightRadius, float lightIntensity)
        {
            //set the G-Buffer parameters
            pointLightEffect.Parameters["colorMap"].SetValue(colourRT.GetTexture());
            pointLightEffect.Parameters["normalMap"].SetValue(normalRT.GetTexture());
            pointLightEffect.Parameters["depthMap"].SetValue(depthRT.GetTexture());

            //compute the light world matrix
            //scale according to light radius, and translate it to light position
            Matrix sphereWorldMatrix = Matrix.CreateScale(lightRadius) * Matrix.CreateTranslation(lightPosition);

            pointLightEffect.Parameters["World"].SetValue(sphereWorldMatrix);
            pointLightEffect.Parameters["View"].SetValue(camera.View);
            pointLightEffect.Parameters["Projection"].SetValue(camera.Projection);

            //light position
            pointLightEffect.Parameters["lightPosition"].SetValue(lightPosition);

            //set the color, radius and Intensity
            pointLightEffect.Parameters["Color"].SetValue(color.ToVector3());
            pointLightEffect.Parameters["lightRadius"].SetValue(lightRadius);
            pointLightEffect.Parameters["lightIntensity"].SetValue(lightIntensity);

            //parameters for specular computations
            pointLightEffect.Parameters["cameraPosition"].SetValue(camera.Position);
            pointLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(camera.View * camera.Projection));

            //size of a halfpixel, for texture coordinates alignment
            pointLightEffect.Parameters["halfPixel"].SetValue(halfPixel);

            //calculate the distance between the camera and light center
            float cameraToCenter = Vector3.Distance(camera.Position, lightPosition);

            //if we are inside the light volume, draw the sphere's inside face
            if (cameraToCenter < (lightRadius + camera.NearPlane))
            {
                GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
            }
            else
            {
                GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
            }

            pointLightEffect.Begin();

            pointLightEffect.Techniques[0].Passes[0].Begin();

            foreach (ModelMesh mesh in sphereModel.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    GraphicsDevice.VertexDeclaration = meshPart.VertexDeclaration;
                    GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer, meshPart.StreamOffset, meshPart.VertexStride);
                    GraphicsDevice.Indices = mesh.IndexBuffer;
                    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshPart.BaseVertex, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
                }
            }

            pointLightEffect.Techniques[0].Passes[0].End();
            pointLightEffect.End();

            GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
        }

        private void DrawLights(GameTime gameTime)
        {
            GraphicsDevice.SetRenderTarget(0, lightRT);

            //clear all components to 0
            GraphicsDevice.Clear(Color.TransparentBlack);

            GraphicsDevice.RenderState.AlphaBlendEnable = true;

            //use additive blending, and make sure the blending factors are as we need them
            GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add;
            GraphicsDevice.RenderState.SourceBlend = Blend.One;
            GraphicsDevice.RenderState.DestinationBlend = Blend.One;

            //use the same operation on the alpha channel
            GraphicsDevice.RenderState.SeparateAlphaBlendEnabled = false;

            GraphicsDevice.RenderState.DepthBufferEnable = false;


            for (int i = 0; i < lights.LightList.Count(); i++)
            {
                DrawPointLight(lights.LightList[i].Position, lights.LightList[i].Colour, lights.LightList[i].Radius, 5);
            }
            
            GraphicsDevice.RenderState.AlphaBlendEnable = false;

            GraphicsDevice.RenderState.DepthBufferEnable = true;

            GraphicsDevice.SetRenderTarget(0, null);

            
            finalCombineEffect.Parameters["colorMap"].SetValue(colourRT.GetTexture());
            finalCombineEffect.Parameters["lightMap"].SetValue(lightRT.GetTexture());
            finalCombineEffect.Parameters["halfPixel"].SetValue(halfPixel);
            
            finalCombineEffect.Begin();
            finalCombineEffect.Techniques[0].Passes[0].Begin();

            //render a full-screen quad
            quadRenderer.Render(Vector2.One * -1, Vector2.One);
            finalCombineEffect.Techniques[0].Passes[0].End();

            finalCombineEffect.End();       
        }

        public override void Draw(GameTime gameTime)
        {
            SetGBuffer();
            ClearGBuffer();

            GraphicsDevice.Clear(Color.Gray);
            environment.DrawEnvironmentDeferred(camera, gameTime);

            ResolveGBuffer();

            DrawLights(gameTime);

            fps = (1000 / gameTime.ElapsedGameTime.TotalMilliseconds);
            fps = Math.Round(fps, 0);
            if (environment.ModelNumber == 1)
            {
                Game.Window.Title = "Deferred Renderer drawing " + environment.ModelNumber + " model with " + lights.LightList.Count + " lights at " + fps.ToString() + " FPS";
            }
            else
            {
                Game.Window.Title = "Deferred Renderer drawing " + environment.ModelNumber + " models with " + lights.LightList.Count + " lights at " + fps.ToString() + " FPS";
            }

            base.Draw(gameTime);
        } 

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            // TODO: Add your update code here

            base.Update(gameTime);
        }
    }
}